import {
  Component,
  OnInit,
  OnChanges,
  Input,
  Output,
  Renderer2,
  ViewChild,
  EventEmitter,
  ElementRef,
  ViewContainerRef,
  TemplateRef,
  ComponentFactoryResolver,
  ChangeDetectionStrategy,
  ViewEncapsulation,
  OnDestroy,
  SimpleChanges,
  Type,
  Injector,
  ComponentRef,
  ApplicationRef,
  HostListener,
  Pipe,
  PipeTransform,
} from '@angular/core';
import { Subject, fromEvent, isObservable } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { trigger, state, style, transition, animate, AnimationEvent, group } from '@angular/animations';
import { TemplatePortal, ComponentPortal, DomPortalOutlet } from '@angular/cdk/portal';
import { Overlay, OverlayConfig } from '@angular/cdk/overlay';
import { isNumber as _isNumber } from 'lodash';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { OverlayService } from '../core/overlay/api';
import { InputBoolean, InputNumber, TypeUtils, doFnResult, isEmpty } from '../core/utils/api';
import { I18NService } from '../core/i18n/api';
import { ModalRef } from '../modal/modalRef';
import {
  MODAL_ADJUST_POSITION_MAP,
  ModalButton,
  ModalCallback,
  ModalType,
  ModalTypeEnum,
  CompatibleDocument,
  genWidthByDrawEnum,
} from '../modal/interface';
import { LvConfig } from '../core/config';
import { LvPositionType } from '../core/types';
import { getPosition, POSITION_MAP } from '../core/tooltipbase/tooltip-overlay-position';
import { MotionDuration, MotionFunc } from '../core/animation/api';
import { drawerAnimations } from './drawer-animations';

@Pipe({
  name: 'SafeHtml',
})
export class SafeHtmlPipe implements PipeTransform {
  constructor(protected dom: DomSanitizer) {}
  public transform(value): SafeHtml {
    return this.dom.bypassSecurityTrustHtml(value);
  }
}

@Component({
  selector: 'lv-modal',
  templateUrl: 'modal.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  animations: [
    trigger('showModal', [
      state(
        'hide',
        style({
          transform: 'scale(0)',
          opacity: 0,
        }),
      ),
      state(
        'show',
        style({
          transform: 'scale(1)',
          opacity: 1,
        }),
      ),
      transition('hide => show', [
        group([
          animate(
            `${MotionDuration.LG} ${MotionFunc.EASE_OUT}`,
            style({
              transform: 'scale(1)',
            }),
          ),
          animate(
            `${MotionDuration.LG} ${MotionFunc.EASE_IN_OUT}`,
            style({
              opacity: 1,
            }),
          ),
        ]),
      ]),
      transition('show => hide', [
        group([
          animate(
            `${MotionDuration.MD} ${MotionFunc.EASE_IN}`,
            style({
              transform: 'scale(0)',
            }),
          ),
          animate(
            `${MotionDuration.MD} ${MotionFunc.EASE_IN_OUT}`,
            style({
              opacity: 0,
            }),
          ),
        ]),
      ]),
      // popover使用不同的state，解决动画过程中元素大小变化导致popover弹窗定位闪跳问题
      state(
        'popoverHide',
        style({
          opacity: 0,
        }),
      ),
      ...drawerAnimations,
    ]),
  ],
})
export class ModalComponent<T> extends ModalRef<T> implements OnInit, OnChanges, OnDestroy {
  @Input() lvClass: string;

  @Input() lvOkTips: string;

  @Input() lvModalKey: string; // When create modal through ModalService, set the key to ensure the uniqueness of the modal

  @Input() lvComponentParams: T; // Only available when content type is a component

  @Input() lvCancelTips: string;

  @Input() lvDialogIcon: string;

  @Input() lvOk: ModalCallback;

  @Input() lvFocusButtonId: string;

  @Input() lvCancel: ModalCallback;

  @Input() lvType: ModalType = 'modal';

  @Input() lvParentModalKeys: string[];

  @Input() lvAfterOpen: ModalCallback;

  @Input() @InputNumber() lvIndex: number;

  @Input() lvBeforeOpen: ModalCallback;

  @Input() lvAfterClose: ModalCallback;

  @Input() lvBeforeClose: ModalCallback;

  @Input() lvWidth: number | string = 'auto';

  @Input() lvHeight: number | string = 'auto';

  @Input() lvPopoverOrigin: ElementRef = this.el; // popover类型触发源点

  @Input() lvOkText: string = this.i18n.get('ok');

  @Input() @InputBoolean() lvVisible: boolean = false;

  @Input() @InputBoolean() lvModality: boolean = true;

  @Input() @InputBoolean() lvEmbedded: boolean = false; // [unrealized] Modal can be embedded into page element whether type is drawer.

  @Input() @InputBoolean() lvOkLoading: boolean = false;

  @Input() @InputBoolean() lvOkDisabled: boolean = false;

  @Input() @InputBoolean() lvScrollBlock: boolean = true;

  @Input() lvCancelText: string = this.i18n.get('cancel');

  @Input() @InputBoolean() lvCancelLoading: boolean = false;

  @Input() lvPopoverPosition: LvPositionType = 'bottomLeft'; // 展示位置，默认bottomLeft，具体设置见POSITION_MAP

  @Input() lvContent: string | TemplateRef<object> | Type<T>; // If not Specified, will use <ng-content></ng-content>

  @Input() @InputBoolean() lvCancelDisabled: boolean = false;

  @ViewChild('modalTitle') modalTitleRef: ElementRef<Element>;

  @ViewChild('resizeHandle') resizeHandle: ElementRef<Element>;

  @Input() lvOkType: 'primary' | 'link' | 'default' = 'primary';

  @Input() @InputBoolean() lvCloseButtonDisplay: boolean = true;

  @ViewChild('modalContent') modalContentRef: ElementRef<Element>;

  @Input() lvHeader: string | TemplateRef<object> | null = 'Title';

  @Output() readonly lvVisibleChange = new EventEmitter<boolean>();

  @Input() lvCancelType: 'primary' | 'link' | 'default' = 'default';

  @ViewChild('modalCloseBtn') modalCloseBtnRef: ElementRef<Element>;

  @ViewChild('modalTemplate', { static: true }) modalTemplate: TemplateRef<any>;

  @Input() @InputBoolean() lvDraggable: boolean = LvConfig.modalOptions.lvDraggable || false; // Draggable whether type is modal/dialog.

  @Input() lvFooter: Array<ModalButton<T>> | TemplateRef<object> | null | 'default' = 'default';

  @Input() lvResize: boolean | { max: number; min: number } = LvConfig.modalOptions.lvResize || false;

  @Input() @InputBoolean() lvKeyboardEscape: boolean = LvConfig.modalOptions.lvKeyboardEscape || false;

  @Input() @InputBoolean() lvMaskTransparent: boolean = LvConfig.modalOptions.lvMaskTransparent || false;

  @Input() lvOkLoadingText: string = LvConfig.modalOptions.lvOkLoadingText || this.i18n.get('loadingText');

  @Input() lvDrawerPositionOffset = LvConfig.modalOptions.lvDrawerPositionOffset || ['0px', '0px', '0px', '0px'];

  @Input() lvCancelLoadingText: string = LvConfig.modalOptions.lvCancelLoadingText || this.i18n.get('loadingText');

  @Input() lvOuterClosable: boolean | { exclusions: HTMLElement[] } = LvConfig.modalOptions.lvOuterClosable || false;

  @Input() lvDrawerPosition: 'top' | 'bottom' | 'left' | 'right' = LvConfig.modalOptions.lvDrawerPosition || 'right';

  _panelRect;
  _panelContent;
  classMap: null;

  public _overlay;

  /** 鼠标是否按下 */
  _isMouseDown = false;

  public footerButtons;
  _animationLock = false; // 动画锁，控制在动画执行完成前，不能执行其他开启关闭操作
  public contentInstance;
  public isNoHeader = true;
  public isNoFooter = true;
  public modalNativeElement;
  public isAnimating = true;
  public isShowView = false;
  public isFirstRender = true;
  public typeUtils = TypeUtils;
  public _animationState: string;
  public modalTypeEnum = ModalTypeEnum;
  public openState$ = new Subject<boolean>();

  private portalHost;

  private _listenerContentScroll;
  private destroy$ = new Subject<void>();
  private mousePosition: {
    x: number;
    y: number;
  } | null;

  constructor(
    private el: ElementRef,
    private renderer: Renderer2,
    private cfr: ComponentFactoryResolver,
    private appRef: ApplicationRef,
    private viewContainerRef: ViewContainerRef,
    private overlay: Overlay,
    private overlayService: OverlayService,
    private i18n: I18NService,
  ) {
    super();
  }

  @HostListener('document:mouseup')
  onMouseUp(): void {
    this._isMouseDown = false;
  }

  @HostListener('document:mousedown', ['$event'])
  onMouseDown(event): void {
    this.resizeDown(event);
  }

  @HostListener('document:mousemove', ['$event'])
  onMouseMove(event): void {
    this.resizePanel(event);
  }

  getActualSize = (parma: number, max: number, min: number): any => {
    if (parma < min) {
      return min;
    } else if (parma > max) {
      return max;
    } else {
      return parma;
    }
  };

  getInstance(): any {
    return this;
  }

  open(): void {
    this.visibleStateChange(true);
  }

  triggerOk(): void {
    this.presetButtonClick('ok');
  }

  getModalElement(): any {
    return this.modalNativeElement;
  }

  triggerCancel(): void {
    this.presetButtonClick('cancel');
  }

  getContentComponent(): any {
    return this.contentInstance;
  }

  close(result?: any): void {
    const _result = result ? result : null;
    this.visibleStateChange(false, _result);
  }

  modalButtonClick(button: ModalButton<T>): void {
    if (button.onClick) {
      button.onClick(this, button);
    }
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.lvVisible) {
      this.modalStateChange(this.lvVisible);
    }
  }

  /**
   * listen animation(start)
   */
  public onAnimationStart(event: AnimationEvent): void {
    if (this._isFormShowToHide(event)) {
      this.isAnimating = true;
    }
  }

  isModalButton(value: TemplateRef<object> | Array<ModalButton<T>> | null | 'default'): boolean {
    return Array.isArray(value) && value.length > 0;
  }

  triggerCustomButton(id): void {
    const button = this.footerButtons.filter((item): boolean => item.id === id);
    this.modalButtonClick(button[0]);
  }

  /**
   * 是否禁用拖拽
   */
  disabledDragging(): boolean {
    return (
      this.lvDraggable === false ||
      this.lvType === this.modalTypeEnum.DRAWER ||
      this.lvType === this.modalTypeEnum.POPOVER
    );
  }

  typeConvert(value): string {
    if (typeof value === 'number') {
      return `${value}px`;
    } else if (typeof value === 'string') {
      return value;
    } else {
      return 'auto';
    }
  }

  mouseClick(e: MouseEvent): void {
    this.mousePosition = { x: e.pageX, y: e.pageY };

    // 100ms 内发生过点击事件，则从点击位置动画展示
    // 否则直接 zoom 展示
    // 这样可以兼容非点击方式展开
    setTimeout((): void => {
      this.mousePosition = null;
    }, 100);
  }

  setPopoverPos(): void {
    this._overlay?.overlayRef?._positionStrategy?.positionChanges.subscribe((e): void => {
      this.lvPopoverPosition = getPosition(e.connectionPair) as LvPositionType;
    });

    // 通知 overlay 刷新视图
    this._overlay?.overlayRef?.updatePosition();
  }

  ngOnDestroy(): void {
    if (this._overlay?.overlayRef.hasAttached()) {
      this._overlay.overlayRef.dispose();
    }

    // clear body scroll listener
    this._listenerContentScroll?.();

    this.destroy$.next();
    this.destroy$.complete();
  }

  ngOnInit(): void {
    fromEvent<KeyboardEvent>(window.document.body, 'keydown')
      .pipe(takeUntil(this.destroy$))
      .subscribe((e): void => this.keydownListener(e));
    fromEvent<MouseEvent>(window.document.body, 'click')
      .pipe(takeUntil(this.destroy$))
      .subscribe((e): void => this.mouseClick(e));
  }

  resizeDown(event): void {
    if (!this.resizeHandle) {
      return;
    }

    if (
      this.resizeHandle.nativeElement === event.target &&
      this.lvType === this.modalTypeEnum.DRAWER &&
      this.lvResize
    ) {
      this._panelContent = this._overlay.overlayRef.overlayElement;
      this._panelRect = this._panelContent.getBoundingClientRect(); // 获取panel面板宽高
      this._isMouseDown = true; // 鼠标按下状态设置为true
    }
  }

  /**
   * listen animation(done)
   */
  public onAnimationDone(event: AnimationEvent): void {
    const flag =
      event.triggerName === 'showModal' &&
      event.fromState !== 'void' &&
      (event.toState === 'show' ||
        event.toState === 'drawerRightIn' ||
        event.toState === 'drawerLeftIn' ||
        event.toState === 'drawerTopIn' ||
        event.toState === 'drawerBottomIn');
    if (flag) {
      this.isAnimating = false;
      this.openState$.next(true);
    }
    if (this._isFormShowToHide(event)) {
      this.openState$.next(false);
    }
  }

  getWidth(): string {
    if (this.lvType === this.modalTypeEnum.DRAWER) {
      const _offsetRight = this.lvDrawerPositionOffset[1];
      const _offsetLeft = this.lvDrawerPositionOffset[3];
      const flag =
        (this.lvDrawerPosition === 'top' || this.lvDrawerPosition === 'bottom') &&
        _offsetLeft !== 'auto' &&
        _offsetRight !== 'auto';
      if (flag) {
        return '100%';
      } else {
        return this.typeConvert(this.lvWidth);
      }
    } else {
      return this.typeConvert(this.lvWidth);
    }
  }

  getHeight(): string {
    if (this.lvType === this.modalTypeEnum.DRAWER) {
      const _offsetTop = this.lvDrawerPositionOffset[0];
      const _offsetBottom = this.lvDrawerPositionOffset[2];
      const flag =
        (this.lvDrawerPosition === 'left' || this.lvDrawerPosition === 'right') &&
        _offsetTop !== 'auto' &&
        _offsetBottom !== 'auto';
      if (flag) {
        return 'auto';
      } else {
        return this.typeConvert(this.lvHeight);
      }
    } else {
      return this.typeConvert(this.lvHeight);
    }
  }

  _isFormShowToHide(event: AnimationEvent): boolean {
    return (
      event.triggerName === 'showModal' &&
      ((event.fromState === 'show' && event.toState === this.getHideAnimationStateKey()) ||
        (event.fromState === 'drawerRightIn' && event.toState === 'drawerRightOut') ||
        (event.fromState === 'drawerLeftIn' && event.toState === 'drawerLeftOut') ||
        (event.fromState === 'drawerTopIn' && event.toState === 'drawerTopOut') ||
        (event.fromState === 'drawerBottomIn' && event.toState === 'drawerBottomOut'))
    );
  }

  presetButtonClick(type): void {
    const triggerFn = { ok: this.lvOk, cancel: this.lvCancel }[type];
    const loading = { ok: 'lvOkLoading', cancel: 'lvCancelLoading' }[type];
    const fn = (): void => {
      this.close({ trigger: type });
    };

    if (!triggerFn) {
      fn();
    } else {
      const call: any = triggerFn(this);
      if (call instanceof Promise || isObservable(call)) {
        this[loading] = true;
      }
      doFnResult(call, (result): void => {
        this[loading] = false;
        if (result) {
          fn();
        }
      });
    }
  }

  updatePopoverPos(): void {
    if (this.lvType !== this.modalTypeEnum.POPOVER) {
      return;
    }

    const overlay = this._overlay.overlayRef.overlayElement;
    const height = {
      header: overlay.querySelector('.lv-modal-header')?.clientHeight,
      body: overlay.querySelector('.lv-modal-body')?.clientHeight,
      footer: overlay.querySelector('.lv-modal-footer')?.clientHeight,
    };

    this.setPopoverPos();
    // 16px 为气泡弹窗与源点之间的距离
    overlay.offsetParent.style.minHeight = `${height.header + height.body + height.footer + 16}px`;
    overlay.offsetParent.style.height = 'auto';
  }

  /**
   * Dynamically configuring the modal input parameters
   * @param options Parameters
   * The following parameters cannot be dynamically configured: lvType、lvModalKey、lvModality、lvScrollBlock、lvOuterClosable、lvMaskTransparent
   */
  setProperty(options): void {
    const posMap = {
      left: 'drawerLeftIn',
      right: 'drawerRightIn',
      top: 'drawerTopIn',
      bottom: 'drawerBottomIn',
    };
    options.lvDrawerPosition && (this._animationState = posMap[options.lvDrawerPosition]); // 位置变更时更新抽屉的动画初始状态

    // If drawer scene, 4 enums of width can be set
    options.lvType = this.lvType;
    genWidthByDrawEnum(options);

    // Update modal parameters
    Object.assign(this, options);

    if (!this.isFirstRender) {
      this.setSizeAndPosition();
      this.setFooter();
      this.setZIndex();
      this.updatePopoverPos();
    }
  }

  /**
   * Listen modal body scroll, change modal header style
   */
  onBodyScroll(e): void {
    const k = Math.min(e.target.scrollTop / 100, 1);
    // Element title margin-top max 24px, min 10px
    const marginTop = 24 - (24 - 10) * k;
    // Element title margin-bottom max 16px, min 10px
    const marginBottom = 16 - (16 - 10) * k;
    // Element close button position top max 26px, min 12px
    const posTop = 26 - (26 - 12) * k;
    this.renderer.setStyle(this.modalTitleRef.nativeElement, 'margin-top', `${marginTop}px`);
    this.renderer.setStyle(this.modalTitleRef.nativeElement, 'margin-bottom', `${marginBottom}px`);
    if (this.modalCloseBtnRef) {
      this.renderer.setStyle(this.modalCloseBtnRef.nativeElement, 'top', `${posTop}px`);
    }
  }

  /**
   * 改变panel宽高
   */
  resizePanel(event): void | boolean {
    // 如果鼠标是按下状态
    if (this._isMouseDown) {
      const screenWidth = window.innerWidth;
      const screenHeight = window.innerHeight;
      let MAX;
      let MIN;
      if (this.lvResize instanceof Object) {
        MAX = this.lvResize.max;
      } else if (this.lvDrawerPosition === 'left' || this.lvDrawerPosition === 'right') {
        MAX = screenWidth;
      } else {
        MAX = screenHeight;
      }
      if (this.lvResize instanceof Object) {
        MIN = this.lvResize.min;
      } else if (this.lvDrawerPosition === 'left' || this.lvDrawerPosition === 'right') {
        MIN = 400;
      } else {
        MIN = 200;
      }
      let { width } = this._panelRect; // 计算panel元素宽度
      let { height } = this._panelRect; // 计算panel元素高度
      const offsetT = this._panelRect.top;
      const offsetR = screenWidth - this._panelRect.right;
      const offsetB = screenHeight - this._panelRect.bottom;
      const offsetL = this._panelRect.left;
      switch (this.lvDrawerPosition) {
        case 'top':
          height = event.y - offsetT;
          this._panelContent.style.height = `${this.getActualSize(height, MAX, MIN)}px`;
          break;
        case 'right':
          width = screenWidth - event.x - offsetR;
          this._panelContent.style.width = `${this.getActualSize(width, MAX, MIN)}px`;
          break;
        case 'bottom':
          height = screenHeight - event.y - offsetB;
          this._panelContent.style.height = `${this.getActualSize(height, MAX, MIN)}px`;
          break;
        case 'left':
          width = event.x - offsetL;
          this._panelContent.style.width = `${this.getActualSize(width, MAX, MIN)}px`;
          break;
        default:
          break;
      }
    }
    return false; // 必须返回false，否则会触发系统事件，导致功能异常
  }

  private closeModal(result): void {
    if (this._overlay?.overlayRef.hasAttached()) {
      this._overlay.close(result);
    }
  }

  // Setup button options
  private setFooter(): void {
    if (this.lvFooter && this.isModalButton(this.lvFooter)) {
      this.initModalButtons(this.lvFooter);
    }
  }

  private getHideAnimationStateKey(): 'popoverHide' | 'hide' {
    return this.lvType === this.modalTypeEnum.POPOVER ? 'popoverHide' : 'hide';
  }

  private setComponentParams(): void {
    if (this.lvComponentParams) {
      Object.assign(this.contentInstance, this.lvComponentParams);
    }
  }

  private keydownListener(event: KeyboardEvent): void {
    // tslint:disable-next-line:deprecation
    if (event.keyCode === 27 && this.lvKeyboardEscape) {
      this.close({ trigger: 'escape' });
    }
  }

  private modalStateChange(visible: boolean, result?: any): void {
    const _result = result ? result : null;
    if (visible) {
      this.openModal();
    } else {
      this.closeModal(_result);
    }
  }

  private modalOverlayAfterClose(result: any): void {
    // Destroys content component when modal is closed
    if (this.lvContent && this.portalHost && !this.portalHost._isDisposed) {
      this.portalHost.detach();
    }

    if (this.lvAfterClose) {
      this.lvAfterClose(result);
    }
  }

  private visibleStateChange(visible: boolean, result?: any): void {
    const _result = result ? result : null;
    if (this.lvVisible !== visible && !this._animationLock) {
      this.lvVisible = visible;
      this.lvVisibleChange.emit(visible);
      this.modalStateChange(visible, _result);
    }
  }

  private setTransformOrigin(el: HTMLElement): void {
    if (!this.mousePosition || this.lvType === 'drawer') {
      return;
    }
    const offset = this.offset(el);
    const child = el.children[0] as HTMLElement;

    child.style.transformOrigin = `${this.mousePosition.x - offset.left}px ${this.mousePosition.y - offset.top}px`;
  }

  private initModalButtons(buttons): void {
    this.footerButtons = buttons.map((button): any => ({
      ...{
        type: 'default',
        size: 'default',
        autoLoading: true,
        loadingText: this.i18n.get('loadingText'),
        loading: false,
        visible: true,
        disabled: false,
      },
      ...button,
    }));
  }

  private offset(el: HTMLElement): { [key: string]: number } {
    const { left, top } = el.getBoundingClientRect();
    const pos = { left, top };
    const doc = el.ownerDocument as CompatibleDocument;
    const win = doc.defaultView || doc.parentWindow;
    pos.left += this.getScroll(win);
    pos.top += this.getScroll(win, true);
    return pos;
  }

  private getScroll(win: Window, top?: boolean): number {
    let result = win[`page${top ? 'Y' : 'X'}Offset`];
    const scrollAttr = `scroll${top ? 'Top' : 'Left'}`;
    if (!_isNumber(result)) {
      const doc = win.document;
      result = doc.documentElement[scrollAttr];
      if (!_isNumber(result)) {
        result = doc.body[scrollAttr];
      }
    }
    return result;
  }

  // Set new z-index for overlay
  private setZIndex(): void {
    if (this.lvIndex) {
      this.lvModality && (this._overlay.overlayRef.backdropElement.style.zIndex = this.lvIndex);
      this.lvType === this.modalTypeEnum.MODAL && (this._overlay.overlayRef.hostElement.style.zIndex = this.lvIndex);
      this.lvType === this.modalTypeEnum.DRAWER &&
        (this._overlay.overlayRef.overlayElement.style.zIndex = this.lvIndex);
    }
  }

  private modalOverlayBeforeOpen(_resolve): void {
    const fn = (resolve): void => {
      resolve();
    };

    if (!this.lvBeforeOpen) {
      fn(_resolve);
    } else {
      const call = this.lvBeforeOpen();
      doFnResult(call, (result): void => {
        if (result) {
          fn(_resolve);
        } else {
          // If beforeOpen prevent opening, reset visible status
          this.visibleStateChange(false);
        }
      });
    }
  }

  /**
   * Execute the animation to show the modal
   */
  private executeShowModalAnimation(): void {
    // Start open animation
    this.setModalAnimationState('show');

    const openSubscribe = this.openState$.subscribe((open): void => {
      if (open) {
        // Get modal nativeElement
        this.modalNativeElement = this._overlay?.overlayRef?.overlayElement?.firstChild;

        if (this.lvAfterOpen) {
          this.lvAfterOpen(this);
        }

        openSubscribe.unsubscribe();

        this.updatePopoverPos();
      }
    });
  }

  private modalOverlayBeforeClose(_resolve, result): void {
    const fn = (resolve): void => {
      this.setModalAnimationState(this.getHideAnimationStateKey());

      // 触发关闭不能控制到外部modal的动画生效，只能通过添加class解决
      const overlayBackDrop = document.getElementsByClassName(`lv-overlay-backdrop-${this.lvModalKey}`)[0];
      overlayBackDrop?.classList?.add('cdk-overlay-transparent-backdrop');

      const closeSubscribe = this.openState$.subscribe((open): void => {
        if (!open) {
          closeSubscribe.unsubscribe();
          resolve();
        }
      });
    };

    if (!this.lvBeforeClose) {
      fn(_resolve);
    } else {
      const call: any = this.lvBeforeClose(this, result);
      doFnResult(call, (_result): void => {
        if (_result) {
          fn(_resolve);
        } else {
          // If beforeClose prevent closing, reset visible status
          this.visibleStateChange(true);
        }
      });
    }
  }

  // 待实现 当前按钮默认聚焦效果不需要，该方法不调用，后续根据设计规范进行修改调用
  private setFocus(): void {
    if (this.lvFocusButtonId) {
      let button;

      if (this.lvFocusButtonId === 'ok') {
        button = '#lvModalOkButton';
      } else if (this.lvFocusButtonId === 'cancel') {
        button = '#lvModalCancelButton';
      } else {
        button = `#ct_${this.lvFocusButtonId}`;
      }
      this.renderer.selectRootElement(button, true).focus();
      return;
    }

    if (this.lvFooter === 'default') {
      // Default focus Ok/Cancel button
      if (this.lvOkText === null && this.lvCancelText === null) {
        return;
      }
      if (this.lvCancelText === null) {
        this.renderer.selectRootElement('#lvModalOkButton', true).focus();
      } else {
        this.renderer.selectRootElement('#lvModalCancelButton', true).focus();
      }
    } else {
      const closeButton = !this.lvCloseButtonDisplay
        ? undefined
        : this.renderer.selectRootElement('#lvModalCloseButton', true);
      closeButton?.focus();
    }
  }

  private modalOverlayAfterOpen(): void {
    // Create dynamic component as content
    this.setComponentAsContent();

    const delay = setTimeout((): void => {
      // start rendering view
      this.isShowView = true;
      this.isFirstRender = false;

      // Set width/height/position for modal
      this.setSizeAndPosition();

      // Setup button options
      this.setFooter();

      // Set new z-index for overlay
      this.setZIndex();

      this.isNoHeader = isEmpty(this.lvHeader);
      this.isNoFooter = isEmpty(this.lvFooter);

      this.executeShowModalAnimation();

      this._animationLock = false;

      // Add listener for body scroll
      if (this.lvType === this.modalTypeEnum.DRAWER && this.typeUtils.isRealString(this.lvHeader)) {
        this._listenerContentScroll = this.renderer.listen(
          this.modalContentRef.nativeElement,
          'scroll',
          (event): void => {
            this.onBodyScroll(event);
          },
        );
      }

      clearTimeout(delay);
    }, 0);
  }

  // Create dynamic component as content
  private setComponentAsContent(): void {
    if (this.typeUtils.isComponent(this.lvContent)) {
      // Inject ModalRef in content component
      const childInjector = Injector.create({
        providers: [{ provide: ModalRef, useValue: this }],
      });

      // Create content component portal
      this.portalHost = new DomPortalOutlet(
        this._overlay.overlayRef.overlayElement.querySelector('.lv-modal-content'),
        this.cfr,
        this.appRef,
        childInjector,
      );

      if (this.portalHost._componentFactoryResolver._parent === null) {
        // eslint-disable-next-line no-console
        console.error("The dynamic component did not find the correct component factory. Please import 'ModalModule'.");
        // "The dynamic component did not find the correct component factory. Please import 'ModalModule'."
      }

      const contentPortal = new ComponentPortal(this.lvContent as Type<T>);
      const portalComponentRef: ComponentRef<any> = this.portalHost.attachComponentPortal(contentPortal);

      this.contentInstance = portalComponentRef.instance;
      this.setComponentParams();
    }
  }

  // Set width/height/position for modal
  private setSizeAndPosition(): void {
    if (this._overlay.overlayRef === undefined) {
      return;
    }

    const _overlayElement = this._overlay.overlayRef.overlayElement;
    const _offsetTop = this.lvDrawerPositionOffset[0];
    const _offsetRight = this.lvDrawerPositionOffset[1];
    const _offsetBottom = this.lvDrawerPositionOffset[2];
    const _offsetLeft = this.lvDrawerPositionOffset[3];
    _overlayElement.style.width = this.getWidth();
    _overlayElement.style.height = this.getHeight();

    this.setTransformOrigin(_overlayElement);

    if (this.lvType === this.modalTypeEnum.DRAWER) {
      _overlayElement.style.top = _offsetTop;
      _overlayElement.style.right = _offsetRight;
      _overlayElement.style.bottom = _offsetBottom;
      _overlayElement.style.left = _offsetLeft;
      if (this.lvDrawerPosition === 'left') {
        _overlayElement.style.right = 'auto';
      }
      if (this.lvDrawerPosition === 'right') {
        _overlayElement.style.left = 'auto';
      }
      if (this.lvDrawerPosition === 'top') {
        _overlayElement.style.bottom = 'auto';
      }
      if (this.lvDrawerPosition === 'bottom') {
        _overlayElement.style.top = 'auto';
      }
    }
  }

  private setModalAnimationState(_state): void {
    this._animationLock = true;

    if (_state === 'show') {
      this._animationState = 'show';
      this.lvType === this.modalTypeEnum.DRAWER &&
        this.lvDrawerPosition === 'right' &&
        (this._animationState = 'drawerRightIn');
      this.lvType === this.modalTypeEnum.DRAWER &&
        this.lvDrawerPosition === 'left' &&
        (this._animationState = 'drawerLeftIn');
      this.lvType === this.modalTypeEnum.DRAWER &&
        this.lvDrawerPosition === 'top' &&
        (this._animationState = 'drawerTopIn');
      this.lvType === this.modalTypeEnum.DRAWER &&
        this.lvDrawerPosition === 'bottom' &&
        (this._animationState = 'drawerBottomIn');
    } else if (_state === this.getHideAnimationStateKey()) {
      this._animationState = this.getHideAnimationStateKey();
      this.lvType === this.modalTypeEnum.DRAWER &&
        this.lvDrawerPosition === 'right' &&
        (this._animationState = 'drawerRightOut');
      this.lvType === this.modalTypeEnum.DRAWER &&
        this.lvDrawerPosition === 'left' &&
        (this._animationState = 'drawerLeftOut');
      this.lvType === this.modalTypeEnum.DRAWER &&
        this.lvDrawerPosition === 'top' &&
        (this._animationState = 'drawerTopOut');
      this.lvType === this.modalTypeEnum.DRAWER &&
        this.lvDrawerPosition === 'bottom' &&
        (this._animationState = 'drawerBottomOut');
    } else {
      return;
    }
  }

  private modalOverlayConfig(): OverlayConfig {
    // overlay 連結點位置参数
    const popoverConnectedPosition = [POSITION_MAP[this.lvPopoverPosition], ...MODAL_ADJUST_POSITION_MAP];
    // 设置位置显示策略
    const globalPos = this.overlay?.position().global().centerHorizontally().centerVertically();
    const positionStrategyMap = {
      [this.modalTypeEnum.MODAL]: globalPos,
      [this.modalTypeEnum.DIALOG]: globalPos,
      [this.modalTypeEnum.DRAWER]: null,
      [this.modalTypeEnum.POPOVER]: this.overlay
        ?.position()
        .flexibleConnectedTo(this.lvPopoverOrigin)
        .withPositions(popoverConnectedPosition),
    };
    const positionStrategy = positionStrategyMap[this.lvType];
    let scrollStrategy = this.overlay?.scrollStrategies.noop();

    if (this.lvScrollBlock) {
      scrollStrategy = this.overlay?.scrollStrategies.block();
    }

    return new OverlayConfig({
      hasBackdrop: this.lvModality,
      backdropClass: [
        `lv-overlay-backdrop-${this.lvModalKey}`,
        this.lvMaskTransparent ? 'cdk-overlay-transparent-backdrop' : 'cdk-overlay-dark-backdrop',
        this.lvType === 'modal' ? 'cdk-overlay-modal-backdrop' : '',
      ],
      minWidth: this.el.nativeElement.clientWidth,
      panelClass: [
        'lv-modal-panel',
        `lv-identify-${this.lvModalKey}`,
        this.lvType === this.modalTypeEnum.POPOVER ? `lv-modal-popover-position-${this.lvPopoverPosition}` : '',
      ],
      positionStrategy,
      scrollStrategy,
    });
  }

  private openModal(): void {
    if (this._overlay?.overlayRef.hasAttached()) {
      return;
    }

    // Initialize animation status
    this.setModalAnimationState(this.getHideAnimationStateKey());

    const extendConfig = {
      clickToCloseStrategy: this.lvOuterClosable,
      scrollToCloseStrategy: false,
      backdropBindClose: this.lvOuterClosable === false ? false : true,
      internalTriggerClose: (e): void => {
        const overlayContainer = document.querySelector('.cdk-overlay-container');
        const isOverlay = this.overlayService.getTargetNode(e.srcElement, overlayContainer);
        const isBackdrop = e.target.classList.toString().includes(`lv-overlay-backdrop-${this.lvModalKey}`);
        let isParent = false;

        if (this.lvParentModalKeys && this.lvParentModalKeys.length > 0) {
          const array = this.lvParentModalKeys.map((item): boolean => {
            const parentModal = document.querySelector(`.lv-identify-${item}`);
            const parentModalBackdrop = document.querySelector(`.lv-overlay-backdrop-${item}`);
            const isParentModal = parentModal ? this.overlayService.getTargetNode(e.srcElement, parentModal) : false;
            const isParentModalBackdrop = parentModalBackdrop
              ? this.overlayService.getTargetNode(e.srcElement, parentModalBackdrop)
              : false;
            return isParentModal || isParentModalBackdrop;
          });
          isParent = array.some((item): boolean => item === true);
        }
        if (isBackdrop || !isOverlay || isParent) {
          this.close({ trigger: 'outer' });
        }
      },
      beforeOpen: (): Promise<any> =>
        new Promise((resolve): void => {
          this.modalOverlayBeforeOpen(resolve);
        }),
      afterOpen: (overlayRef): void => {
        this._overlay = overlayRef;
        this.modalOverlayAfterOpen();
      },
      beforeClose: (_overlayRef, _portal, result: any): Promise<any> =>
        new Promise((resolve): void => {
          this.modalOverlayBeforeClose(resolve, result);
        }),
      afterClose: (result: any): void => {
        this.modalOverlayAfterClose(result);
      },
    };

    const portal = new TemplatePortal(this.modalTemplate, this.viewContainerRef);

    this.overlayService.open(portal, this.modalOverlayConfig(), extendConfig);
  }
}
